// 
// Entify project
//  
// Author:
//       Lauri Taimila <lauri@taimila.com>
// 
// Copyright (c) 2010 Lauri Taimila
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

using Cairo;
using System;
using System.Collections.Generic;

namespace MonoDevelop.EntifyEditor
{
	/// <summary>
	/// Implements entity set canvas. Entity set is visually illustrated
	/// on canvas. Canvas also allows user to do some simple modifications
	/// to the illustrated diagram.
	/// </summary>
	public class EntitySetCanvasWidget : Gtk.DrawingArea
	{
		/// <summary>
		/// Entity set canvas fires this event if it changes active entity.
		/// </summary>
		public delegate void ActiveChanged();
    	public event ActiveChanged ActiveEntityChanged;

		/// <summary>
		/// Entity set that is illustrated by this canvas object.
		/// </summary>
		private EntitySet model;

		/// <summary>
		/// Flag that can be used to prevent focus loss form entity canvas.
		/// If this is set true, then canvas won't release focus until the
		/// flag has been set back to false.
		/// </summary>
		private bool preventFocusLoss;

		/// <summary>
		/// This is set true when first mouse button is clicked on entity
		/// and false when the button is released.
		/// </summary>
		private bool entityDragEnabled;

		/// <summary>
		/// This is set true when first mouse button is clicked on entity resize area
		/// and false when the button is released.
		/// </summary>
		private bool entityResizeEnabled;

		/// <summary>
		/// Define mouse cursor position related to active entity's position (top-left corner)
		/// </summary>
		private double dragXoffset;
		private double dragYoffset;

		/// <summary>
		/// Grid size in pixels.
		/// </summary>
		private readonly int gridSize = 15;

		/// <summary>
		/// Creates a new instance of EntitySetCanvas.
		/// </summary>
		/// <param name="model">
		/// Entity set to display on canvas. <see cref="EntitySet"/>
		/// </param>
		public EntitySetCanvasWidget (EntitySet model) : base()
		{
			this.model = model;
			this.CanFocus = true;
			this.HasFocus =true;
			this.CanDefault = true;
			this.HasDefault = true;
			this.Events = Gdk.EventMask.AllEventsMask;

			this.KeyPressEvent += new Gtk.KeyPressEventHandler(this.OnKeyPressEvent);
			this.MotionNotifyEvent += new Gtk.MotionNotifyEventHandler(this.OnMotionNotifyEvent);
			this.ButtonReleaseEvent += new Gtk.ButtonReleaseEventHandler(this.OnButtonReleaseEvent);

			// Prevents arrow buttons to change focus to another widget
			this.FocusOutEvent += delegate {
				if(preventFocusLoss)
				{
					this.GrabFocus();
					this.preventFocusLoss = false;
				}
			};
		}

		/// <summary>
		/// Called when keyboard key is pressed and entity canvas has focus.
		/// </summary>
		protected virtual void OnKeyPressEvent(object o, Gtk.KeyPressEventArgs args)
		{
			if(this.model.ActiveEntity != null)
			{
				double newX = this.model.ActiveEntity.Point.X;
				double newY = this.model.ActiveEntity.Point.Y;

				switch(args.Event.Key)
				{
					case Gdk.Key.Up:
						if(this.model.ActiveEntity.Point.Y % this.gridSize > 0.5) // Snap to grid
						{
							newY = this.model.ActiveEntity.Point.Y - (this.model.ActiveEntity.Point.Y % this.gridSize) + 0.5;
							this.model.ActiveEntity.Point = new PointD(newX, newY);
						}
						else
						{
							if(newY - this.gridSize <= 0.5)
							{
								newY = 0.5;
							}
							else
							{
								newY = newY - this.gridSize;
							}
						}
						break;

					case Gdk.Key.Down:
						if(this.model.ActiveEntity.Point.Y % this.gridSize > 0.5) // Snap to grid
						{
							newY = this.model.ActiveEntity.Point.Y + (this.model.ActiveEntity.Point.Y % this.gridSize) + 0.5;

							if(newY + this.model.ActiveEntity.Size.Y > this.Allocation.Size.Height)
							{
								newY = newY - (newY % this.gridSize) - this.gridSize + 0.5;
							}

							this.model.ActiveEntity.Point = new PointD(newX, newY);
						}
						else
						{
							if(newY + this.model.ActiveEntity.Size.Y + this.gridSize < this.Allocation.Size.Height)
							{
								newY = newY + this.gridSize;
							}
						}
						break;

					case Gdk.Key.Left:
						if(this.model.ActiveEntity.Point.X % this.gridSize > 0.5) // Snap to grid
						{
							newX = this.model.ActiveEntity.Point.X - (this.model.ActiveEntity.Point.X % this.gridSize) + 0.5;
							this.model.ActiveEntity.Point = new PointD(newX, newY);
						}
						else
						{
							if(newX - this.gridSize <= 0.5)
							{
								newX = 0.5;
							}
							else
							{
								newX = newX - this.gridSize;
							}
						}
						break;
	
					case Gdk.Key.Right:
						if(this.model.ActiveEntity.Point.X % this.gridSize > 0.5) // Snap to grid
						{
							newX = this.model.ActiveEntity.Point.X + (this.model.ActiveEntity.Point.X % this.gridSize) + 0.5;

							if(newX + this.model.ActiveEntity.Size.X > this.Allocation.Size.Width)
							{
								newX = newX - (newX % this.gridSize) - this.gridSize + 0.5;
							}

							this.model.ActiveEntity.Point = new PointD(newX, newY);
						}
						else
						{
							if(newX + this.gridSize + this.model.ActiveEntity.Size.X < this.Allocation.Size.Width)
							{
								newX = newX + this.gridSize;
							}
						}
						break;
				}

				this.model.ActiveEntity.Point = new PointD(newX, newY);
				this.preventFocusLoss = true;
				this.QueueDraw();
			}
		}

		/// <summary>
		/// Called when mouse is clicked on entity canvas.
		/// </summary>
		protected override bool OnButtonPressEvent (Gdk.EventButton ev)
		{
			if(!this.IsFocus)
			{
				this.GrabFocus();
			}

			Entity entity = this.GetEntityFromCoordinates(ev.X, ev.Y);

			if(entity != this.model.ActiveEntity)
			{
				this.model.ActiveEntity = entity;
				this.ActiveEntityChanged();
			}

			if(entity != null)
			{
				if(this.IsMouseOverResizeArea(ev.X, ev.Y))
				{
					this.entityResizeEnabled = true;
				}
				else
				{
					this.dragXoffset = ev.X - this.model.ActiveEntity.Point.X;
					this.dragYoffset = ev.Y - this.model.ActiveEntity.Point.Y;
					this.entityDragEnabled = true;
				}
			}

			this.QueueDraw();

			return base.OnButtonPressEvent(ev);
		}

		/// <summary>
		/// Called when mouse button is released. Deactivates drag and resize modes.
		/// </summary>
		protected virtual void OnButtonReleaseEvent (object o, Gtk.ButtonReleaseEventArgs args)
		{
			this.dragXoffset = 0;
			this.dragYoffset = 0;
			this.entityDragEnabled = false;
			this.entityResizeEnabled = false;
		}

		/// <summary>
		/// Called when mouse moves on canvas. Method relocates currently active
		/// entity when drag flag is true.
		/// </summary>
		protected virtual void OnMotionNotifyEvent (object o, Gtk.MotionNotifyEventArgs args)
		{
			if(this.HasFocus && this.entityResizeEnabled)
			{
				this.ResizeEntity(args);
			}
			else if(this.HasFocus && this.entityDragEnabled)
			{
				this.DragEntity(args);
			}
		}
		
		/// <summary>
		/// Drag active entity on canvas. Moves entity to new position by current
		/// mouse cursor position.
		/// </summary>
		private void DragEntity (Gtk.MotionNotifyEventArgs args)
		{
			double newX = args.Event.X - this.dragXoffset;
			double newY = args.Event.Y - this.dragYoffset;

			// If statements below check that entity box stays inside visible grid area.
			if(newX <= 0)
			{
				newX = 0.5;
			}

			if(newY <= 0)
			{
				newY = 0.5;
			}

			if(newX + this.model.ActiveEntity.Size.X >= this.Allocation.Size.Width)
			{
				newX = this.Allocation.Size.Width - this.model.ActiveEntity.Size.X - 0.5;
			}

			if(newY + this.model.ActiveEntity.Size.Y >= this.Allocation.Size.Height)
			{
				newY = this.Allocation.Size.Height - this.model.ActiveEntity.Size.Y - 0.5;
			}

			this.model.ActiveEntity.Point = new PointD(newX, newY);
			this.QueueDraw();
		}
		
		/// <summary>
		/// Resizes active entity on canvas.
		/// </summary>
		private void ResizeEntity (Gtk.MotionNotifyEventArgs args)
		{
			double newWidth = args.Event.X - this.model.ActiveEntity.Point.X - 0.5;
			double newHeight = args.Event.Y - this.model.ActiveEntity.Point.Y - 0.5;

			// If statements below check that entity box stays inside visible grid area.
			if(newWidth <= 100)
			{
				newWidth = 100;
			}

			if(newHeight <= 50)
			{
				newHeight = 50;
			}

			if(newWidth + this.model.ActiveEntity.Point.X >= this.Allocation.Size.Width)
			{
				newWidth = this.Allocation.Size.Width - this.model.ActiveEntity.Point.X - 0.5;
			}

			if(newHeight + this.model.ActiveEntity.Point.Y >= this.Allocation.Size.Height)
			{
				newHeight = this.Allocation.Size.Height - this.model.ActiveEntity.Point.Y - 0.5;
			}

			this.model.ActiveEntity.Size = new PointD(newWidth, newHeight);
			this.QueueDraw();
		}

		protected override void OnSizeAllocated (Gdk.Rectangle allocation)
		{
			base.OnSizeAllocated (allocation);
			// Insert layout code here.
		}

		protected override void OnSizeRequested (ref Gtk.Requisition requisition)
		{
			// Calculate desired size here.
		}

		/// <summary>
		/// Called when canvas needs to be redrawn.
		/// </summary>
		protected override bool OnExposeEvent (Gdk.EventExpose ev)
		{
			base.OnExposeEvent (ev);

			int currentWidth = 0;
			int currentHeight = 0;
			ev.Window.GetSize(out currentWidth, out currentHeight);

			List<RelationInformation> relationInfos = new List<RelationInformation>();

			using (Context g = Gdk.CairoHelper.Create(ev.Window))
			{
				EntityCanvasDrawer.DrawGrid(g, currentWidth, currentHeight, this.gridSize);

				foreach(Entity entity in this.model.Entities)
				{
					foreach(var relation in entity.Relations)
					{
						relationInfos.Add(new RelationInformation(relation.Name, entity, this.model.GetEntity(relation.Type), relation.ToMany));
					}
				}

				foreach(RelationInformation relation in relationInfos)
				{
					EntityCanvasDrawer.DrawRelation(g, relation);
				}

				foreach(Entity entity in this.model.Entities)
				{
					EntityCanvasDrawer.DrawEntity(g, entity, entity == this.model.ActiveEntity);
				}
			}

			return true;
		}

		/// <summary>
		/// Returns entity from the given coordinates. If multiple entities
		/// are found on the same coordinates then the top one is returned.
		/// If given coordinates doesn't contain any entity, null is returned.
		/// </summary>
		private Entity GetEntityFromCoordinates(double x, double y)
		{
			if(this.model == null || this.model.Entities == null)
			{
				return null;
			}

			for(int i = this.model.Entities.Count - 1; i >= 0; i--)
			{
				Entity e = this.model.Entities[i];
				if(x >= e.Point.X && x <= e.Point.X + e.Size.X &&
				   y >= e.Point.Y && y <= e.Point.Y + e.Size.Y)
				{
					return e;
				}
			}

			return null;
		}

		/// <summary>
		/// Checks is cursor over currently active entity's resize area.
		/// </summary>
		private bool IsMouseOverResizeArea(double cursorX, double cursorY)
		{
			double x = this.model.ActiveEntity.Point.X + this.model.ActiveEntity.Size.X - 20;
			double y = this.model.ActiveEntity.Point.Y + this.model.ActiveEntity.Size.Y - 20;
			double x2 = this.model.ActiveEntity.Point.X + this.model.ActiveEntity.Size.X;
			double y2 = this.model.ActiveEntity.Point.Y + this.model.ActiveEntity.Size.Y;

			if(cursorX > x && cursorX < x2 && cursorY > y && cursorY < y2)
			{
				return true;
			}
			else
			{
				return false;
			}
		}
	}
}
